/*
 * Copyright 2012 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.codec;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ByteProcessor;

import java.util.List;

/**
 * 换行解码器
 *
 * @author 二十
 */
public class LineBasedFrameDecoder extends ByteToMessageDecoder {
    /*当前业务层面指定的数据帧的最大长度。，如果客户端发来的数据报超过此长度，解码器需要抛异常。*/
    private final int maxLength;
    /*是否快速失败，默认是false。*/
    private final boolean failFast;
    /*是否跳过分隔符字节，默认是false。*/
    private final boolean stripDelimiter;
    /*当前模式是否是丢弃模式*/
    private boolean discarding;
    /*记录丢弃模式的时候，当前已经丢弃的数据量。*/
    private int discardedBytes;
    /*上一次从堆积区的扫描位点，只有堆积区是业务层的半包的时候才有值。*/
    private int offset;

    public LineBasedFrameDecoder(final int maxLength) {
        this(maxLength, true, false);
    }

    public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
        this.maxLength = maxLength;
        this.failFast = failFast;
        this.stripDelimiter = stripDelimiter;
    }

    /**
     * @param ctx 上下文
     * @param in  堆积区
     * @param out 集合
     * @throws Exception
     */
    @Override
    protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object decoded = decode(ctx, in);
        if (decoded != null) {
            out.add(decoded);
        }
    }


    protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
        //去堆积区里面查找换行符的位置，可能返回 -1  或者  一个 大于等于 0 的值。
        // -1 表示当前堆积区未查找到换行符。
        final int eol = findEndOfLine(buffer);
        //条件成立说明是正常模式，不是丢弃模式
        if (!discarding) {
            //如果在堆积区里面查找到了换行符
            if (eol >= 0) {
                //最终指向业务层面的数据帧
                final ByteBuf frame;
                //计算出当前 业务层数据报的长度
                final int length = eol - buffer.readerIndex();
                //表示换行符的长度，如果是 /n 就是1 ， /r/n就是 2 。
                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
                //条件成立，说明业务层的数据包长度>=maxLength，说明本次数据报的长度大于业务层指定的最大包长度，需要跳过并且抛异常。
                if (length > maxLength) {
                    //将整个数据包跳过
                    buffer.readerIndex(eol + delimLength);
                    fail(ctx, length);
                    return null;
                }
                //执行到这里说明满足规则，判断一下堆积区的换行符是否需要跳过
                if (stripDelimiter) {
                    frame = buffer.readRetainedSlice(length);
                    buffer.skipBytes(delimLength);
                } else {
                    frame = buffer.readRetainedSlice(length + delimLength);
                }
                //返回正常切片出来的数据帧
                return frame;
                //堆积区内没有找到换行符
            } else {
                //获取堆积区可用数据量的长度
                final int length = buffer.readableBytes();
                //如果长度大于等于MaxLength，说明当前堆积区里面剩余的数据已经超过最大帧长度，需要开始丢弃了。
                //堆积区里面是半包数据
                if (length > maxLength) {
                    discardedBytes = length;
                    //跳过堆积区里面的数据
                    buffer.readerIndex(buffer.writerIndex());
                    //开启丢弃模式
                    discarding = true;
                    offset = 0;
                    //判断是不是快速失败
                    if (failFast) {
                        fail(ctx, "over " + discardedBytes);
                    }
                }
                //返回null，没有从堆积区解析出数据。
                return null;
            }
        } else {
            //执行到这里，说明当前decoder处于丢弃模式


            //检查是否在剩下的数据里面查找到eol标记
            //条件成立，说明当前堆积区已经查找到换行符了，需要丢弃半包数据，并且设置decoder为正常模式。
            if (eol >= 0) {
                final int length = discardedBytes + eol - buffer.readerIndex();
                final int delimLength = buffer.getByte(eol) == '\r' ? 2 : 1;
                buffer.readerIndex(eol + delimLength);
                discardedBytes = 0;
                discarding = false;
                if (!failFast) {
                    fail(ctx, length);
                }
            } else {
                //执行到这里，说明仍然未查找到换行符，需要继续丢弃数据，并保持丢弃模式
                discardedBytes += buffer.readableBytes();
                buffer.readerIndex(buffer.writerIndex());
                // We skip everything in the buffer, we need to set the offset to 0 again.
                offset = 0;
            }
            return null;
        }
    }

    private void fail(final ChannelHandlerContext ctx, int length) {
        fail(ctx, String.valueOf(length));
    }

    private void fail(final ChannelHandlerContext ctx, String length) {
        ctx.fireExceptionCaught(
                new TooLongFrameException(
                        "frame length (" + length + ") exceeds the allowed maximum (" + maxLength + ')'));
    }

    private int findEndOfLine(final ByteBuf buffer) {
        //获取堆积区的总长度
        int totalLength = buffer.readableBytes();
        //遍历堆积区的每一个字节，指定开始位置和结束位置 查找 /n 返回换行符在堆积区里面的位置。
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        //说明在堆积区里面查找到了换行符
        if (i >= 0) {
            offset = 0;
            //条件成立说明在堆积区内查找到换行符是/r/n，而不是/n那么简单。
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            //没找到，需要设置偏移量为总长度，为什么？因为当前堆积区是一个半包业务数据，下次堆积区再次累加新数据之后，在调用当前decoder需要从offset开始检查换行符，提高效率。
            offset = totalLength;
        }
        return i;
    }
}
